using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace Common.Utilities.Persistence
{
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class XmlFilePersistenceUtil<T> : ISerializable<T>
        where T : IXmlSerializable
    {
        #region ISerializable<T> Members
        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <param name="entity"></param>
        public void Save(string path, ref T entity)
        {
            string xmlFilePath = path;
            XmlDocument xDoc = new XmlDocument();
            string rootElementName = this.GetRootElementName(entity.XPath);
            if(System.IO.File.Exists(xmlFilePath))
            {
                xDoc.Load(xmlFilePath);
            }
            else
            {
                xDoc.LoadXml("<" + rootElementName + "></" + rootElementName + ">");
            }

            string idValue = EntityFieldMappingHelper.GetPropertyValue(entity, entity.IDAttributeName);
            bool isNewEntity = false;
            if (string.IsNullOrEmpty(idValue))
                isNewEntity = true;
            else
            {
                switch (entity.IDType)
                {
                    case IDFieldType.String:
                        if (string.IsNullOrEmpty(idValue))
                            isNewEntity = true;
                        break;
                    case IDFieldType.GUID:
                        Guid guid = new Guid(idValue);
                        if (guid.Equals(Guid.Empty))
                            isNewEntity = true;
                        break;
                    case IDFieldType.Integer:
                        int idIntValue = int.Parse(idValue);
                        if (idIntValue <= 0)
                            isNewEntity = true;
                        break;
                }
            }

            if (isNewEntity)
            {
                switch (entity.IDType)
                {
                    case IDFieldType.String:
                        EntityFieldMappingHelper.GetPropertyByName(entity, entity.IDAttributeName).SetValue(entity, string.Empty, null);
                        break;
                    case IDFieldType.GUID:
                        EntityFieldMappingHelper.GetPropertyByName(entity, entity.IDAttributeName).SetValue(entity, Guid.NewGuid(), null);
                        break;
                    case IDFieldType.Integer:
                        int id = this.GetLastIDForEntity(xDoc, entity) + 1;
                        EntityFieldMappingHelper.GetPropertyByName(entity, entity.IDAttributeName).SetValue(entity, id, null);
                        break;
                }
            }
            //List<string> parentNodeNames = this.GetParentElementNames(entity.XPath);
            
            //XmlNode parentNode = xDoc.DocumentElement;
            //string parentPath = xDoc.DocumentElement.Name;
            //if (parentNodeNames != null && parentNodeNames.Count > 1)
            //{
            //    for (int i = 1; i < parentNodeNames.Count; i++)
            //    {
            //        //parentPath += "/" + parentNodeNames[i];
            //        //parentNode = XmlDataUtil.UpdateElemant(ref xDoc, parentPath, parentNodeNames[i]);
            //    }
            //}
            XmlNode xNode = XmlDataUtil.UpdateElemant(ref xDoc, entity.XPath, entity.GetType().Name);
            List<Tuple> fields = EntityFieldMappingHelper.GetSimpleFields(entity);
            for (int i = 0; i < fields.Count; i++)
            {
                if (fields[i].Value != null)
                {
                    XmlDataUtil.UpdateAttribute(ref xDoc, xNode, fields[i].Name, fields[i].Value);
                }
            }
            xDoc.Save(xmlFilePath);

            List<IXmlSerializable> compositeFields = EntityFieldMappingHelper.GetXmlSerializableCompositeFields(entity);
            if (compositeFields != null && compositeFields.Count > 0)
            {
                for (int i = 0; i < compositeFields.Count; i++)
                {
                    IXmlSerializable compositeField = compositeFields[i];
                    XmlFilePersistenceUtil<IXmlSerializable> filePersistencer = new XmlFilePersistenceUtil<IXmlSerializable>();
                    filePersistencer.Save(xmlFilePath, ref compositeField);
                }
                xDoc = new XmlDocument();
                xDoc.Load(xmlFilePath);
            }
            xDoc.Save(xmlFilePath);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        public T GetByID(string path, int id)
        {
            T entity = Activator.CreateInstance<T>();
            if (!System.IO.File.Exists(path))
                throw new Exception("XML Data file [" + path + "] does not exist");
            
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(path);
            string idValue = EntityFieldMappingHelper.GetPropertyValue(entity, entity.IDAttributeName);
            if (entity.IDType != IDFieldType.Integer)
                idValue = "\"" + idValue + "\"";
            XmlNode xNode = xDoc.SelectSingleNode(entity.XPath + "[@" + entity.IDAttributeName +
                "=" + idValue + "]");
            if (xNode != null)
            {
                List<NameType> fields = EntityFieldMappingHelper.GetFieldMappings(entity);
                for (int i = 0; i < fields.Count; i++)
                {
                    XmlAttribute att = (XmlAttribute)xNode.Attributes.GetNamedItem(fields[i].Name);
                    if (att != null)
                    {
                        string fieldValue = att.Value;
                        EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, fieldValue);
                    }
                    else
                    {
                        EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, null);
                    }
                }
                //List<IXmlSerializable> compositeFields = EntityFieldMappingHelper.GetXmlSerializableCompositeFields(entity);
                //if (compositeFields != null && compositeFields.Count > 0)
                //{
                //    foreach (IXmlSerializable compositeField in compositeFields)
                //    {
                //        XmlFilePersistenceUtil<IXmlSerializable> filePersistencer = new XmlFilePersistenceUtil<IXmlSerializable>();
                //        filePersistencer.Save(xmlFilePath, ref compositeField);
                //    }
                //}
            }

            return entity;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <param name="id"></param>
        public void Delete(string path, int id)
        {
            T entity = Activator.CreateInstance<T>();
            if (!System.IO.File.Exists(path))
                throw new Exception("XML Data file [" + path + "] does not exist");
            
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(path);
            string idValue = EntityFieldMappingHelper.GetPropertyValue(entity, entity.IDAttributeName);
            if (entity.IDType != IDFieldType.Integer)
                idValue = "\"" + idValue + "\"";
            XmlNode xNode = xDoc.SelectSingleNode(entity.XPath + "[@" + entity.IDAttributeName +
                "=" + idValue + "]");
            if (xNode != null)
            {
                xNode.ParentNode.RemoveChild(xNode);
            }
            xDoc.Save(path);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public List<T> GetAll(string path)
        {
            List<T> entities = new List<T>();
            
            T entityType = Activator.CreateInstance<T>();
            if (!System.IO.File.Exists(path))
                throw new Exception("XML Data file [" + path + "] does not exist");

            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(path);
            XmlNodeList xNodes = xDoc.SelectNodes(entityType.XPath);
            if (xNodes != null && xNodes.Count>0)
            {
                foreach (XmlNode xNode in xNodes)
                {
                    T entity = Activator.CreateInstance<T>();
                    List<NameType> fields = EntityFieldMappingHelper.GetFieldMappings(entity);
                    for (int i = 0; i < fields.Count; i++)
                    {
                        XmlAttribute att = (XmlAttribute)xNode.Attributes.GetNamedItem(fields[i].Name);
                        if (att != null)
                        {
                            string fieldValue = att.Value;
                            EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, fieldValue);
                        }
                        else
                        {
                            EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, null);
                        }
                    }
                    entities.Add(entity);
                }
            }

            return entities;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="path"></param>
        /// <param name="filters"></param>
        /// <returns></returns>
        public List<T> FindAll(string path, List<FieldFilter> filters)
        {
            List<T> entities = new List<T>();

            T entityType = Activator.CreateInstance<T>();
            if (!System.IO.File.Exists(path))
                throw new Exception("XML Data file [" + path + "] does not exist");

            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(path);
            XmlNodeList xNodes = xDoc.SelectNodes(entityType.XPath);
            if (xNodes != null && xNodes.Count > 0)
            {
                foreach (XmlNode xNode in xNodes)
                {
                    T entity = Activator.CreateInstance<T>();
                    List<NameType> fields = EntityFieldMappingHelper.GetFieldMappings(entity);
                    for (int i = 0; i < fields.Count; i++)
                    {
                        XmlAttribute att = (XmlAttribute)xNode.Attributes.GetNamedItem(fields[i].Name);
                        if (att != null)
                        {
                            string fieldValue = att.Value;
                            EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, fieldValue);
                        }
                        else
                        {
                            EntityFieldMappingHelper.SetPropertyValue(entity, fields[i].Name, null);
                        }
                    }
                    if (filters != null && filters.Count > 0)
                    {
                        bool passFilter = true;
                        for (int i = 0; i < filters.Count; i++)
                        {
                            string subjValue = EntityFieldMappingHelper.GetPropertyValue(entity, filters[i].FieldName);
                            passFilter = filters[i].Evaluate(subjValue);
                            if (!passFilter)
                                break;
                        }

                        if(passFilter)
                            entities.Add(entity);
                    }
                    else
                    {
                        entities.Add(entity);
                    }
                }
            }

            return entities;
        }
        #endregion

        #region util
        private string GetRootElementName(string xPath)
        {
            List<string> parentElementNames = this.GetParentElementNames(xPath);
            if (parentElementNames != null && parentElementNames.Count > 0)
                return parentElementNames[0];
            else
            {
                string rootElemName = xPath;
                if (xPath.IndexOf("/") > 0)
                    rootElemName = xPath.Substring(0, xPath.IndexOf("/"));
                if (rootElemName.IndexOf("[") > 0)
                    rootElemName = rootElemName.Substring(0, rootElemName.IndexOf("["));
                return rootElemName;
            }
        }

        private List<string> GetParentElementNames(string xPath)
        {
            // remove attribute path
            System.Text.StringBuilder buffer = new StringBuilder();
            int pos = 0;
            int matchLeftPos = xPath.IndexOf("[");
            while (matchLeftPos >= 0)
            {
                int matchRightPos = xPath.IndexOf("]", matchLeftPos + 1);
                if (matchRightPos > 0)
                {
                    buffer.Append(xPath.Substring(pos, matchLeftPos - pos));
                    pos = matchRightPos + 1;
                    matchLeftPos = xPath.IndexOf("[", pos);
                }
                else
                {
                    break;
                }
            }
            if (pos < xPath.Length)
            {
                buffer.Append(xPath.Substring(pos));
            }
            xPath = buffer.ToString();
            string[] elements = xPath.Split(new char[] { '/' });
            List<string> parentElementNames = new List<string>();
            if (elements.Length > 1)
            {
                for (int i = 0; i < elements.Length - 1; i++)
                {
                    parentElementNames.Add(elements[i]);
                }
            }
            return parentElementNames;
        }

        private int GetLastIDForEntity(XmlDocument xDoc, T entity)
        {
            if (entity.IDType != IDFieldType.Integer)
                throw new Exception("Only integer ID type can be used in this method");

            int lastID = 0;
            XmlNodeList nodes = xDoc.SelectNodes(entity.XPath);
            if (nodes != null && nodes.Count > 0)
            {
                foreach (XmlNode node in nodes)
                {
                    int id = int.Parse(node.Attributes.GetNamedItem(entity.IDAttributeName).Value);
                    if (id > lastID)
                        lastID = id;
                }
            }
            return lastID;
        }
        #endregion
    }
}
